/*
 * SerialGPSConnection.java
 *
 * Created on 23 mai 2006, 09:27
 *
 */

package courselog.gpsutils;

import courselog.CourseLogAppForm;
import ewe.io.File;
import ewe.io.FileOutputStream;
import ewe.io.IOException;
import ewe.io.IOHandle;
import ewe.io.SerialPort;
import ewe.io.SerialPortOptions;
import ewe.io.Stream;
import ewe.sys.Convert;
import ewe.sys.HandleStoppedException;
import ewe.sys.mThread;
import ewe.ui.ReportException;
//import ewe.util.EventDispatcher;
import ewe.ui.EventListener;

/**
 * Handles a GPS connected to a Serial port.
 * Generates Measurements (MeasuredPoint) each time a new measurement
 * is issued by the receiver.
 * Handles a listener to the connection to receive all new points.
 * Configuration of the device is planned but ot implemented :
 * - enable differential measurements (WAAS or EGNOS) when available
 * - initialize for fast re-start
 * - power saving mode
 * @author C. Rigal
 */
public class SerialGPSConnection implements Runnable {
    
    /**
     * Receiver status is fine!
     */
    public static final int STATUS_OK=0;
    /**
     * GPs is connected via the serial port but the GPS produces no valid fix.
     */
    public static final int STATUS_GPSLOST=1;
    /**
     * GPS receiver does not transmit through the serial port.
     */
    public static final int STATUS_NOK=-1;
    
    /**
     * Maximum serial port buffer size (256 chars).
     * If bigger, data is blocked long before being exploited,
     * if smaller, partial NMEA sentences are received and not usable.
     * NMEA specifies that the sentences should not exceed 80 chars.
     */
    public static int BUFFMAX=1024;
    
    /**
     * Number of lines to buffer before parsing NMEA.
     */
    public static int NBLINESINBUFFER=8;
    
    /**
     * Maximum number of satellites status to record (20).
     */
    public static int NBSATMAX=20;
    
    /**
     * Identifier of a GGA NMEA sentence ("GPGGA").
     */
    protected static final byte[] bGPGGA= new String("GPGGA").getBytes();
    /**
     * Values sequence in a GGA sentence ("VVCVCIIVVCVCVI").
     */
    protected static final String fGPGGA="VVCVCIIVVCVCVI";
    /**
     * Identifier of a RMC NMEA sentence ("GPRMC").
     */
    protected static final byte[] bGPRMC= new String("GPRMC").getBytes();
    /**
     * Values sequence in a RMC sentence ("VCVCVCVVIVVC").
     */
    protected static final String fGPRMC="VCVCVCVVIVVC";
    /**
     * Identifier of a GSV NMEA sentence ("GPGSV").
     */
    protected static final byte[] bGPGSV= new String("GPGSV").getBytes();
    /**
     * Values sequence in a GSV sentence ("IIIIIIIIIIIIIIIIIII").
     */
    protected static final String fGPGSV="IIIIIIIIIIIIIIIIIII";
    
    /**
     * For debugging, GPS data may be logged into a file. ("tmpGPSLog.txt")
     */
    public static String logFileName="tmpGPSLog.txt";
    /**
     * Flag indicating that the receiver should log its data.
     */
    public static boolean isLogging = false;
    
    /**
     * Flag indicating that the receive should read from a "tmpGPSLog.txt" file
     * in order to emulate the presence of a receiver.
     */
    public static boolean isEmulating=false;
    
    /**
     * Debugging counter to verify problems with connections
     */
    public int debugCounter=0;
    
    /**
     * Stream to output the logged GPS data to.
     */
    protected FileOutputStream outLog;
    /**
     * Current point received from the GPS.
     * Same object is used, fields are updated depending on the received
     * data.
     */
    public MeasuredPoint currentPoint=new MeasuredPoint();
    /**
     * Status of the receiver (one of STATUS_XXX values).
     */
    public int status = STATUS_NOK;
    /**
     * Thread reading the GPS receiver values and issueing events when
     * a new point is decoded.
     */
    public mThread reader;
    /**
     * Port options that were used to open the serial port to the GPS.
     */
    public SerialPortOptions currentSPO;
    /**
     * Input reader of the GPS serial port.
     */
    public Stream inGPS;
    
//    protected EventDispatcher dispatcher=new EventDispatcher(1);
    /**
     * Event passed to listeners when a new point is decoded.
     * Same object is reused.
     */
    protected ReceiverEvent myPointEvent=new ReceiverEvent();
    /**
     * Event passed to listeners when the status of the receiver changes.
     * Same object is reused.
     */
    protected ReceiverEvent myStatusEvent=new ReceiverEvent();
    
    /**
     * Listener to this receiver.
     */
    protected EventListener myListener;
    
    /**
     * Array containing integer values decoded from NMEA (20 ints).
     */
    public int[] vNMEAInt = new int[20];
    /**
     * Array containing double values decoded from NMEA (20 double).
     */
    public double[] vNMEAVal = new double[20];
    /**
     * Array containing single char values decoded from NMEA (20 char).
     */
    public char[] vNMEACar = new char[20];
    
    /**
     * State of the GPS, idenfiers of the satellites currently available.
     */
    public int[] vSatPRN= new int[NBSATMAX];
    /**
     * State of the GPS, azimtuhs of the satellites currently available.
     */
    public int[] vSatAz= new int[NBSATMAX];
    /**
     * State of the GPS, elevations of the satellites currently available.
     */
    public int[] vSatEl= new int[NBSATMAX];
    /**
     * State of the GPS, signal to noise of the satellites currently available.
     */
    public int[] vSatSNR= new int[NBSATMAX];
    
    /**
     * Factory for serial connection creating an active connection.
     * Call this method instead of the constructor and get a connection
     * generating data.
     * If this method is called within an mThread, it tries to reconnect every 2s
     * an only returns if interrupted.
     * Otherwise (not in an mThread) returns a new connection even if connection fails.
     * @param spo Serial ort options to use for the connection
     * @param listener Listener for GPS events
     * @return A new instance of a SerialGPSConnection with an active
     * connection to a GPS device. <B>null</B> if the connection
     * process is interrupted.
     */
    public static SerialGPSConnection getAnActiveSerialGPSConnection(SerialPortOptions spo, EventListener listener) {
        while(true) {
            SerialGPSConnection sGC= new SerialGPSConnection(spo, listener);
            sGC.startReading();
            if(sGC.status!= STATUS_NOK) {
                return sGC;
            }else{
//                if(mThread.inThread()) {
//                    try {
//                        mThread.sleep(2000);
//                    } catch (InterruptedException ex) {
//                        return null;
//                    }
//                }else{
                return sGC;
//                }
            }
        }
    }
    
    /**
     * Creates a new instance of SerialGPSConnection
     * @param spo Serial port options to use for connection to the GPS.
     * @param listener Listener which will receive the state and points events.
     */
    public SerialGPSConnection(SerialPortOptions spo, EventListener listener) {
        //DistanceForm.logger.addToLog("\nConnection- essai // ");
        currentSPO= spo;
        this.myListener = listener;
        myPointEvent.type=ReceiverEvent.NEWPOINT;
        myPointEvent.target=currentPoint;
        myStatusEvent.type=ReceiverEvent.STATUSCHANGED;
        myStatusEvent.target=this;
        status=STATUS_NOK;
    }
    
    /**
     * Starts the GPS reading thread <I>myReader</I>.
     * @return true is the thread started, false otherwise.
     */
    public boolean startReading() {
        int oldStatus = this.status; // Save status
        
        reader = new mThread(this); // Start reader thread
        reader.setPriority(mThread.MIN_PRIORITY);
        reader.start();
        //DistanceForm.logger.addToLog("\nGPS connection started");
        return (this.status!=STATUS_NOK);
    }
    
    /**
     * Method reading the GPS actived in thread <I>reader</I>.
     * This method also parses NMEA values and activates the listener in the
     * reader thread. May only be stopped by an interruption on reader thread.
     */
    public void run() {
        byte[] buff = new byte[BUFFMAX];
        int firstC=0;
        int nbCInBuff=0;
        byte[] bLg= new byte[4];
        boolean isConnected=false;
        int oldStatus= this.status;
        IOHandle readHandle= new IOHandle();
        
        //DistanceForm.logger.addToLog("\nReader process started");
        if(isLogging) {
            boolean tstLog=startLog();
            if(!tstLog) {
                this.status=STATUS_NOK;
                //DistanceForm.logger.addToLog("StartLog rate // ");
            }
        }
        
        try {
            while(true) {
                boolean isNotConnected=true;
                //DistanceForm.logger.addToLog("\ntry to connect again "+(this.debugCounter)+" connection :"+this.hashCode());
                this.debugCounter++;
                while(isNotConnected) {
                    try {
                        if( isEmulating ) {
                            isLogging=false;
                            File fLog= new File(logFileName);
                            inGPS=fLog.toReadableStream();
                        } else {
                            inGPS = currentSPO.connect();
                            //DistanceForm.logger.addToLog("Connect() // ");
                            ((SerialPort)inGPS).setFlowControl(SerialPort.HARDWARE_FLOW_CONTROL);
//                            ((SerialPort)inGPS).setFlowControl(SerialPort.SOFTWARE_FLOW_CONTROL); //SerialPort.HARDWARE_FLOW_CONTROL);
                        }
                        this.status = STATUS_OK;
                        isNotConnected=false;
                    }catch(IOException exc) {
                        this.status=STATUS_NOK;
                        //DistanceForm.logger.addToLog("\ncould not connect");
                        if( this.status!=oldStatus) {
                            this.myListener.onEvent(myStatusEvent);
                            oldStatus=this.status;
                        }
                        mThread.sleep(2000);
                    }
                }
                
                if( this.status!=oldStatus) {
                    if(myListener!=null) {
                        this.myListener.onEvent(myStatusEvent);
                    }
                    oldStatus=this.status;
                }
                
                isConnected=true;
                //DistanceForm.logger.addToLog("\nStart read");
//                boolean hasSlept=false;
                while(isConnected) {
                    try{
                        int nbC=0;
                        if(isEmulating) {
                            mThread.sleep(1000); // 0.1 sec between points
                            if( nbCInBuff==0 ) {
                                int nb=inGPS.read(bLg, 0, 4);
                                if(nb==0) {
                                    throw new IOException("Error reading emulation file : empty");
                                } else {
                                    nbCInBuff=Converters.valByteHex(bLg,0,4);
                                }
                            }
                            if( (BUFFMAX-firstC)<nbCInBuff ) {
                                nbC=inGPS.read(buff, firstC, BUFFMAX-firstC);
                            } else {
                                nbC=inGPS.read(buff, firstC, nbCInBuff);
                            }
                            nbCInBuff -= nbC;
                        } else {
                            try {
                                int numNewLines=NBLINESINBUFFER;
                                int startB=firstC;
                                while((numNewLines>0)&&((startB+1)<BUFFMAX)) {
//                            CourseLogAppForm.dbgL.addToLog("Read Port "+firstC+" has slept="+hasSlept+"\n");
//                            CourseLogAppForm.dbgL.addToLog("Read Port fC="+firstC+" startB="+startB+"\n");
                                    inGPS.readBytes(buff, startB, BUFFMAX-startB, readHandle, false);
//                            nbC=inGPS.read(buff, firstC, BUFFMAX-firstC); Synchronous read blocking when GPS is off
                                    if(readHandle.waitOnAny(IOHandle.Success,5000)) {
                                        nbC =startB+readHandle.bytesTransferred;
                                        for(int iB=startB; iB<nbC; iB++) {
                                            if(buff[iB]=='\n') numNewLines--;
                                        }
                                        startB +=readHandle.bytesTransferred;
//                                    CourseLogAppForm.dbgL.addToLog("trans="+readHandle.bytesTransferred+" new lines="+numNewLines+" nbC="+nbC+"\n");
                                    } else {
                                        this.status = STATUS_NOK;
                                        CourseLogAppForm.dbgL.addToLog("trans KO\n");
                                        isConnected=false;
                                        if( this.status!=oldStatus) {
                                            this.myListener.onEvent(myStatusEvent);
                                            oldStatus=this.status;
                                        }
                                        nbC=firstC;
                                        //DistanceForm.logger.addToLog("\nAsync reading failed");
                                    }
                                }
                                nbC -=firstC;
                            } catch (HandleStoppedException ex) {
                                this.status = STATUS_NOK;
                                isConnected=false;
                                CourseLogAppForm.dbgL.addExceptionToLog(ex);
                                if( this.status!=oldStatus) {
                                    this.myListener.onEvent(myStatusEvent);
                                    oldStatus=this.status;
                                }
                                return;
                            }
                        }
                        if(nbC>0) {
                            if(isLogging) {
                                Converters.intToHexBytes(nbC, bLg, 0, 4);
                                outLog.write(bLg);
                                outLog.write(buff, firstC, nbC);
                            }
                            int nbConsummed = parseNMEAContent(buff, firstC+nbC);
                            firstC = firstC+nbC-nbConsummed;
                            System.arraycopy(buff, nbConsummed, buff, 0, firstC);
//                            hasSlept=mThread.nap(200);
                        }
                    } catch (IOException exc) {
                        this.status = STATUS_NOK;
                        isConnected=false;
                        CourseLogAppForm.dbgL.addExceptionToLog(exc);
                        if( this.status!=oldStatus) {
                            this.myListener.onEvent(myStatusEvent);
                            oldStatus=this.status;
                        }
                        if(isLogging) {
                            stopLog();
                        }
                    }
                }
            }
        } catch (InterruptedException exc) {
            this.status = STATUS_NOK;
            //DistanceForm.logger.addToLog("\nInterrupted Exception "+exc.toString());
            if(isConnected) {
                inGPS.close();
            }
            if( this.status!=oldStatus) {
                this.myListener.onEvent(myStatusEvent);
                oldStatus=this.status;
            }
            return;
        }
    }
    
    /**
     * Reads in the buffer to find NMEA sentences
     * and analyses them in case there are.
     * Also checks the checksum of each sentence.
     * @param b Buffer to read in.
     * @param len Length of usable chars in the buffer, starting at 0.
     * @return Number of chars consummed by the analysis of NMEA
     * sentences.
     */
    public int parseNMEAContent( byte[] b, int len) {
        int idxC=0;
        int idxStar=-1;
        int idxDollar=-1;
        int idxConsumed=0;
        byte chkS=0;
        byte[] bChkS= new byte[2];
        
        
        while( idxC< len ) {
            if(idxStar<0 && idxDollar>=0 && b[idxC]!='*') { // compute checksum between '$' and '*'
                chkS ^= b[idxC];
            }
            
            if( b[idxC]=='$' ) {
                idxDollar=idxC;
                idxConsumed=idxDollar;
                chkS=0;
            }
            if( (b[idxC]=='*')&&(idxDollar>=0)) {
                if( (len-idxC)<=2 ) { // Not enough in buffer to get the checksum value
                    return idxDollar; // all is consumed before the mast '$'
                }
                idxStar = idxC;
                idxConsumed=idxStar+2;
                if( isCheckSumOK(chkS, b, idxC+1)) {
                    analyseNMEAValues(b, idxDollar+1, idxStar-1);
                } else {
//                    DistanceForm.logger.addToLog("\nChecksum error at "+String.valueOf(idxDollar));
//                    try {
//                        mThread.sleep(4000);
//                    } catch (InterruptedException ex) {
//                        new ReportException(ex, "Sleep interrupted again!", null, false).execute();
//                    }
                }
                idxStar=-1;
                idxDollar=-1;
            }
            idxC++;
        }
        if( idxDollar<0 ) { // No start found since last sentence
            idxConsumed = idxC;
        }
        return idxConsumed;
    }
    
    /**
     * When a valid sentence is received, analyses it to update the current
     * point values.
     * <PRE>
     * GGA : get UTC, lat, lon, nbsats, alt, hdop
     * RMC : get date, speed, heading
     * GSV : updates satellite status (no event is generated
     *      but data is available for polling).</PRE>
     * When a point is fully analysed, generate a ReceiverEvent.
     * If fix is not valid, modify status accordingly.
     * @param b Buffer to read in.
     * @param iSt Index of starting location of the NMEA sentence.
     * @param iEnd Index of ending location of the sentence.
     */
    public void analyseNMEAValues(byte[] b, int iSt, int iEnd) {
        //DistanceForm.logger.addToLog("\nTo analyse : %"+new String(b,iSt, iEnd-iSt+1)+"%");
        // Look for the start of a known sentence
        boolean flGPGGA=true;
        boolean flGPRMC=true;
        boolean flGPGSV=true;
        
        for( int idxC=0; idxC<bGPGGA.length; idxC++) {
            if(bGPGGA[idxC]!=b[idxC+iSt]) {
                flGPGGA=false;
            }
            if(bGPRMC[idxC]!=b[idxC+iSt]) {
                flGPRMC=false;
            }
            if(bGPGSV[idxC]!=b[idxC+iSt]) {
                flGPGSV=false;
            }
        }
        if( flGPGGA ) {
            Converters.parseCSVSentence(fGPGGA, b, iSt+bGPGGA.length+1, iEnd,
                    vNMEAVal, vNMEAInt, vNMEACar);
            //DistanceForm.logger.addToLog("\nTime :"+String.valueOf(vNMEAVal[0]));
            if(vNMEAInt[0]!=0) { // fix is valid
                if( status == STATUS_GPSLOST ) { // Dispatch this good news
                    status=STATUS_OK;
                    myListener.onEvent(myStatusEvent);
                }
                if( currentPoint.time != vNMEAVal[0] ) { // hand-over between points
                    currentPoint.initialise(); // Recycle object instead of re-allocating
                    currentPoint.time = vNMEAVal[0]; // first comes the date of measurement
                    currentPoint.computeTime();
                }
                currentPoint.lat = Converters.nMEA2Deg(vNMEAVal[1]); // Latitude abs value
                if(vNMEACar[0]=='S') { // For holidays in my case!
                    currentPoint.lat = -currentPoint.lat;
                }
                currentPoint.lon = Converters.nMEA2Deg(vNMEAVal[2]);
                if(vNMEACar[1]=='W') { // You're west from Greenwich Village
                    currentPoint.lon = -currentPoint.lon;
                }
                currentPoint.satsInSolution = vNMEAInt[1];
                currentPoint.hDOP = vNMEAVal[3];
                currentPoint.altSeaLevel = vNMEAVal[4];
                currentPoint.altGeoid = vNMEAVal[5];
                if( currentPoint.heading >=0 ) { // The point already has a heading, so is complete
                    myListener.onEvent(myPointEvent);
                }
            }else{
                // Set the time anyway
                currentPoint.time = vNMEAVal[0]; // keep the date of measurement
                currentPoint.computeTime();
                if( status == STATUS_OK ) {
                    status=STATUS_GPSLOST;
                    myListener.onEvent(myStatusEvent);
                } else {
                    myListener.onEvent(myPointEvent);
                }
            }
        }
        if( flGPRMC ) {
            Converters.parseCSVSentence(fGPRMC, b, iSt+bGPGGA.length+1, iEnd,
                    vNMEAVal, vNMEAInt, vNMEACar);
            if(vNMEACar[0]!='V') { // fix is valid
                if( status == STATUS_GPSLOST ) { // Dispatch this good news
                    status=STATUS_OK;
                    myListener.onEvent(myStatusEvent);
                }
                if( currentPoint.time != vNMEAVal[0] ) { // hand-over between points
                    currentPoint.initialise(); // Recycle object instead of re-allocating
                    currentPoint.time = vNMEAVal[0]; // first comes the date of measurement
                    currentPoint.computeTime();
                }
                // Latitude and longitude are coming from GGA sentences, we save time
                currentPoint.speed = vNMEAVal[3]*1.855d; // convert knots to km/h
                currentPoint.heading = vNMEAVal[4];
                currentPoint.date= vNMEAInt[0];
                currentPoint.computeTime();
                if( currentPoint.altSeaLevel >0 ) { // The point already has an altitude, so is complete
                    myListener.onEvent(myPointEvent);
                }
            }else{
                if( status == STATUS_OK ) { // Dispatch this bad news
                    status=STATUS_GPSLOST;
                    myListener.onEvent(myStatusEvent);
                }
            }
        }
        if( flGPGSV ) {
            Converters.parseCSVSentence(fGPGSV, b, iSt+bGPGGA.length+1, iEnd,
                    vNMEAVal, vNMEAInt, vNMEACar);
            if(vNMEAInt[1]==1) {
                for(int idx=vSatPRN.length-1; idx>=0; idx-- ) {
                    vSatPRN[idx]=-1;
                }
            }
            int nbSatInSentence = vNMEAInt[2]-4*(vNMEAInt[1]-1); // 4 satellites per line
            if( nbSatInSentence>4 ) nbSatInSentence=4;
            int idxV= nbSatInSentence*4+3-1;
            for( int idxS=nbSatInSentence+(vNMEAInt[1]-1)*4-1; idxS>=(vNMEAInt[1]-1)*4; idxS--) {
                vSatSNR[idxS] = vNMEAInt[idxV];
                idxV--;
                vSatAz[idxS] = vNMEAInt[idxV];
                idxV--;
                vSatEl[idxS] = vNMEAInt[idxV];
                idxV--;
                vSatPRN[idxS] = vNMEAInt[idxV];
                idxV--;
            }
        }
    }
    
    /**
     * Stop the GPS reception.
     * Stops the thread and close the log if any.
     */
    public void close() {
        if( reader!=null)  this.reader.interrupt();
        stopLog();
    }
    
    /**
     * Start to log received data fom the serial port.
     * Data is appended to the file which only grows...
     * @return true if correct, false otherwise.
     */
    public boolean startLog() {
        try{
            //DistanceForm.logger.addToLog("StartLog() // ");
            this.outLog = new FileOutputStream(logFileName,true);
            isLogging = true;
            //DistanceForm.logger.addToLog("FileOutputStream OK // ");
        }catch(IOException exc) {
            new ReportException(exc,"Error starting to log GPS data",null,false).execute();
            isLogging = false;
        }
        return isLogging;
    }
    
    /**
     * Stops logging GPS data.
     */
    public void stopLog() {
        try {
            if(isLogging) {
                if(outLog!=null) {
                    this.outLog.close();
                }
            }
        } catch (IOException exc) {
            new ReportException(exc,"Error stopping to log GPS data",null,false).execute();
        }
    }
    
    /**
     * Verify checksum of an NMEA sentence.
     * Checksum is a XOR of all characters after the '$' and before the '*'.
     * @param valChk Computed checksum of the sentence.
     * @param charChk Hexadecimal value of the checksum read from the port.
     * @param idxChk Index of the checksum value in <I>charChk</I>.
     * @return True if valChk equals interpretation of charChk.
     */
    public boolean isCheckSumOK(int valChk, byte[] charChk, int idxChk) {
        return (valChk==Converters.valByteHex(charChk, idxChk, 2));
    }
    
    /**
     * Interprets a string with port name, rate, etc, into serial port options.
     * Example : "{name=COM6:,rate=4800,bits=8,parity=0,stop=1}"
     * @param sO String containing the description of the options.
     * @return Initialised new Serial port options.
     */
    public static SerialPortOptions getPortOptionsFromString(String sO) {
        SerialPortOptions forSPO=new SerialPortOptions();
        int iD, iE;
        iD=sO.indexOf("{name=")+"{name=".length();
        iE=sO.indexOf(",",iD);
        forSPO.portName=sO.substring(iD,iE);
        iD=sO.indexOf("rate=",iE)+"rate=".length();
        iE=sO.indexOf(",",iD);
        forSPO.baudRate=Integer.parseInt(sO.substring(iD,iE));
        iD=sO.indexOf("bits=",iE)+"bits=".length();
        iE=sO.indexOf(",",iD);
        forSPO.bits=Integer.parseInt(sO.substring(iD,iE));
        iD=sO.indexOf("parity=",iE)+"parity=".length();
        iE=sO.indexOf(",",iD);
        forSPO.parity=Integer.parseInt(sO.substring(iD,iE));
        iD=sO.indexOf("stop=",iE)+"stop=".length();
        iE=sO.indexOf("}",iD);
        forSPO.stopBits=Integer.parseInt(sO.substring(iD,iE));
        
        return forSPO;
    }
    
    /**
     * Provides a set of default port options.
     * Corresponds to what works with my configuration :
     * {name=COM6:,rate=9600,bits=8,parity=0,stop=1}
     * @return Default port options.
     */
    public static SerialPortOptions getDefaultPortOptions() {
        SerialPortOptions  spo= new SerialPortOptions();
        spo.portName="COM6:";
        spo.baudRate=4800;
        spo.bits=8;
        spo.parity=SerialPort.NOPARITY;
        spo.stopBits=1;
        
        return spo;
    }
    
    /**
     * Format <I>forSPO</I> in a string that will be readable later.
     * @param forSPO Options to format.
     * @return String that may be parsed later to reconfigure options.
     */
    public static String getPortOptionsAsString(SerialPortOptions forSPO) {
        StringBuffer sb=new StringBuffer();
        sb.append("{name=");
        sb.append(forSPO.portName);
        sb.append(",rate=");
        sb.append(Convert.formatInt(forSPO.baudRate));
        sb.append(",bits=");
        sb.append(Convert.formatInt(forSPO.bits));
        sb.append(",parity=");
        sb.append(Convert.formatInt(forSPO.parity));
        sb.append(",stop=");
        sb.append(Convert.formatInt(forSPO.stopBits));
        sb.append("}");
        return sb.toString();
    }
    
    /**
     * Get UTC provided by the GPS receiver.
     * @return Time from receiver as a string "HH:MM:SS"
     */
    public String getUTCFromGPS() {
        return Converters.formatUTC(currentPoint.time);
    }
    
    /**
     * Sets the listener of this receiver.
     * Should not be null if an event occurs.
     * @param list Listener to dispatch events to.
     */
    public void setListener(EventListener list) {
        myListener=list;
    }
////===================================================================
//    public void addListener(EventListener e)
////===================================================================
//    {
//        dispatcher.addListener(e);
//    }
////===================================================================
//    public void removeListener(EventListener e)
////===================================================================
//    {
//        dispatcher.removeListener(e);
//    }
}
